/**
   Copyright 2010 Carlo Alberto Degli Atti (lord kada)

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package it.concept.pin.proxy;

import it.concept.pin.PinManager;
import it.concept.pin.annotations.CollectionMap;
import it.concept.pin.annotations.Model;
import it.concept.pin.annotations.Parameters;
import it.concept.pin.annotations.ProxyAdapter;
import it.concept.pin.exceptions.PinException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class ProxyWrapper implements InvocationHandler {

	private Object target;

	public static Object createProxy( Class<?> interfaceName, Object target ) throws InstantiationException, IllegalAccessException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {

		Object targetObject = target;
		
		ProxyAdapter proxyAnnotation = targetObject.getClass().getAnnotation( ProxyAdapter.class );
		
		if ( proxyAnnotation != null ) {
			
			Constructor<?> proxyConstructor = proxyAnnotation.value().getConstructor( new Class<?>[] { targetObject.getClass() } );
		
			targetObject = proxyConstructor.newInstance( targetObject );
			
		}
			
		return Proxy.newProxyInstance( interfaceName.getClassLoader(), new Class[] { interfaceName }, new ProxyWrapper( targetObject ) );

	}

	/**
	 * Private constructor
	 * @param target
	 */
	private ProxyWrapper( Object target ) {

		this.target = target;
		
	}
	
	@SuppressWarnings("unchecked")
	public Object invoke( Object proxy, Method method, Object[] args ) {

		InvokedMethodResult invokedMethodResult = null;

		Class<?> argClasses[] = getClasses( args );

		try {

			try {
				
				invokedMethodResult = invokePojoMethod( target, method.getName(), args );
				
			} catch ( NoSuchMethodException e ) {
				
				if ( target instanceof AbstractProxyAdapter<?> ) 

					invokedMethodResult = invokePojoMethod( ( ( AbstractProxyAdapter<?> ) target ).getPojo(), method.getName(), argClasses );
					
				else
					
					throw e;
					
			}
				
			Object result = invokedMethodResult.getResult();
			
			if ( result != null ) {
				
				Model pinAnnotation = result.getClass().getAnnotation( Model.class );
				
				if ( pinAnnotation != null ) 
					
					return PinManager.toModel( result );
					
				else
					
					if ( result instanceof Collection<?> ) {
						
						CollectionMap annotation = invokedMethodResult.getInvokedMethod().getAnnotation( CollectionMap.class );
						
						Collection<?> resultCollection = ( Collection<?> ) result;
						
						if ( resultCollection.size() > 0 ) {

							Model resultPinAnnotation = resultCollection.toArray()[0].getClass().getAnnotation( Model.class );
							
							if ( resultPinAnnotation != null ) {
	
								Collection<Object> newInstance = (Collection<Object>) ( ( annotation != null && annotation.model() != null && Collection.class.isAssignableFrom( annotation.model() ) ) ? PinManager.newInstance( annotation.model() ) : PinManager.newInstance( invokedMethodResult.getResult().getClass() ) );

								for ( Object item : resultCollection )
									
									newInstance.add( PinManager.toModel( item ) );
								
								return newInstance;
								
							}

						}
						
					}
				
			}
			
			return result;

		} catch ( Exception e ) {

			throw new RuntimeException( e );
			
		}

	}

	private Class<?>[] getClasses( Object[] args ) {
		
		Class<?>[] argClasses = null;
		
		if ( args != null ) {

			argClasses = new Class<?>[ args.length ];
			
			for ( int i = 0; i < args.length; i++ ) 
				
				argClasses[ i ] = args[ i ].getClass();

		}
		 
		return argClasses;
		
	}

	private InvokedMethodResult invokePojoMethod( Object pojo, String name, Object[] args ) throws NoSuchMethodException, PinException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
		
		InvokedMethodResult invokedMethodResult = new InvokedMethodResult();
		
		if ( pojo != null ) {
			
			Class<?> pojoClass = pojo.getClass();
			
			List<Method> pojoMethods = getMethods( pojoClass );
			
			for ( int i = 0; i < pojoMethods.size(); i++ ) {
				
				Method method = pojoMethods.get( i );
				
				if ( method.getName().compareTo( name ) == 0 ) {

					if ( args != null && ( method.getParameterTypes().length != args.length ) || ( args == null && method.getParameterTypes().length > 0 ) )
						 
						continue;
					
					Parameters parameterAnnotation = method.getAnnotation( Parameters.class );
					
					boolean found = false;
					
					Object[] invocationArgs = args;
					
					if ( parameterAnnotation != null ) {

						if ( isTypeMatches( getClasses( args ), parameterAnnotation.value() ) ) {
							
							invocationArgs = new Object[ args.length ];
							
							Class<?>[] pojoParametersTypes = method.getParameterTypes();
							
							for ( int j = 0; j < args.length; j++ ) 
							
								invocationArgs[ j ] = PinManager.toPojo( pojoParametersTypes[ j ], args[ j ] );
						
							found = true;
							
						}
						
					}
					
					if ( ! found && isTypeMatches( getClasses( args ), method.getParameterTypes() ) ) {
							
						found = true;
						
					}
					
					
					if ( found ) {
					
						invokedMethodResult.setResult( method.invoke( pojo, invocationArgs ) );
						invokedMethodResult.setInvokedMethod( method );
						return invokedMethodResult;
					
					}
					
				}
				
			}
				
		}
		
		throw new NoSuchMethodException( name );
		
	}

	static boolean isTypeMatches( Class<?>[] typeA, Class<?>[] typeB ) {
		
		if ( typeA == null ) 
		
			return typeB == null || typeB.length == 0;
		    
		if ( typeB == null ) 
	    
			return typeA == null || typeA.length == 0;
		    
		if ( typeA.length != typeB.length ) 
	    
			return false;
	        
		for ( int i = 0; i < typeB.length; i++ )
	    
			if ( ! typeB[ i ].isAssignableFrom( typeA[ i ] ) ) 
		    
				return false;
		        
		return true;
	    
	}
	  
	private static List<Method> getMethods( Class<?> type ) {
		
		return _getMethods( new ArrayList<Method>(), type );
		
	}
	
	private static List<Method> _getMethods( List<Method> methods, Class<?> type ) {
		
		if ( type.getSuperclass() != null ) 

			_getMethods( methods, type.getSuperclass() );
		
	    for ( Method method: type.getDeclaredMethods() )
	        
	    	methods.add( method );

	    return methods;
	    
	}
	
}